Dart 3 隨著 Flutter 3.10 發布,進行了一次大改版,達成了 100% sound-null safety,代表所有的屬性、變數都要聲明是否為 nullable,它的作用能讓專案的編譯更有效率、速度更快,也能讓專案的穩定性提升,減少不必要的錯誤和崩潰發生。另外也新增了幾種語言特性 Record、Pattern、Class-Modifier 等等,在開發上給予很大的幫助,在許多情境的使用上更簡單而且可讀性高。
本文主要跟大家分享 Dart 3 給予的好處,希望可以幫助大家快速了解它,並且從中受益。所以我在後面準備了很多實際案例,從基本認識、初階到進階使用,分享一些我覺得很棒的神奇用法,讓我們趕快進入正題吧!
Record 是一個匿名且不可變的聚合類型,它可以將多個物件集中在一個物件裡面,一般的使用方式 (doube lat , double lon)
,使用小括號包裹來認定。可以將它們存儲在變數、將它放入 List、將它當作 Map 的 Key,或者在 Record 中包含其他 Record,用法上非常豐富。
當我們有了 Record,在某些時候就不需要為了單一流程創建新的 Class 來紀錄資料。例如:位置有經緯度、顏色有RGB數值,都能夠很簡單的透過 Record 幫忙。而我們也能藉此解決 function 需要多個回傳值的需求。馬上來看以下範例:
getLocation()
比較常見的情景會是回傳經緯度,這裡透過 Record 去處理,並針對需求決定數值是否有自定義名稱,根據可讀性可自行調整,外部透過 (double, double)
去使用// Function
(double Lat, double lon) getLocation(String name) => (25.034092, lon: 121.563956);
// Named-args
({double lat, double lon}) location;
location = (lat: 25.034092, Lon: 121.563956);
// Mixed
var person = ('Yii', isMale: true, '175');
print(person.$1);
print(person.isMale) ;
print(person.$2);
Flutter 常見場景,APP 下方需要有底部選單,也就是 BottomNavigationBar
,但我們不需要創建一個 BottomNavigationBarItem 類別來紀錄名稱以及 Icon 兩個屬性,可以直接使用 Record 代替。而我們在撰寫 UI code 的時候,可以透過編號存取匿名變數
List<(Widget, String)> items = <(Widget, String)>[
(const Icon(Icons.home), 'Home'),
(const Icon(Icons.search), 'Search'),
(const Icon(Icons.face), 'Profile'),
];
BottomNavigationBar(
items: items
.map
((Widget, String) item) =>
BottomNavigationBarItem(
icon: item.$1,
label: item.$2,
),
)
.toList(),
)
Record 本身的 identity 就是依賴擁有的欄位、匿名以及命名,當兩個 Record 結構一樣時就會是相等的
==
operator 的 Object list 進行比對會無法相等test('Records equality', () {
// 1.
const ({int width, int height}) a = (width: 100, height: 200);
const ({int width, int height}) b = (width: 100, height: 200);
const (int width, int height) c = (100, 200);
expect(a, equals(b)); // Passed
expect(a, equals(c)); // Failed
// 2.
final complex = (1, 'dog', ['cat', 'pig']);
final complex2 = (1, 'dog', ['cat', 'pig']);
expect(complex, equals(complex2)); // Failed
});
Pattern Matching 負責檢查 Object 和期望的結構格式是否匹配,符合的話可以存取全部屬性或是部分資料,同時進行了解構,和提高可讀性。實際看範例會更快了解:
此範例的需求是存取 Json 中的指令欄位。可以先看到左邊的舊式寫法,我們需要先檢查 json 是否為 Map、長度以及是否有正確的 Key,接著檢查個別欄位的型別,再將 value 拿出來使用。過程中經過重重關卡已經寫了10行 code,不覺得有點累嗎?
我們看看右邊 Dart 3 新寫法,透過 if-case matching 檢查 json 的結構是否符合兩個欄位,且型別是 String 跟 int,而且不是空值,單純這一行代表著很多條件,而當他們都符合後我就能安心拿值使用。這邊使用到了解構,所以可以直接拿其中的變數使用,總共只寫了2行而已
final json = {'name': 'Amy', 'age': 30};
// Old
if (json is Map<String, Object?> &&
json.length == 2 &&
json.containsKey('name') &&
json.containsKey('age')) {
if (json['name'] is String && json['age'] is int) {
final name = json['name'] as String;
final age = json['age'] as int;
print('User $name is $age years old.');
}
}
// New
if (json case {'name': String name, 'age': int age}) {
print('$name is $age years old.');
} else if (json case {'name': 'Amy', 'age': int age}) {
print('Amy is $age years old.');
} else {
print('Error: json is not correct.');
}
解構用法,同時進行結構比對,符合的話直接使用,不需要額外再宣告新的變數
final names = [
Person(name: 'Yii', age: 27),
Person(name: 'Andy', age: 30),
Person(name: 'Jay', age: 24),
]
final [yii, andy, jay] = names;
print(yii.toString());
print(andy.toString());
print(jay.toString());
更輕量的解構方式,當你想要直接使用原本 Record 裡的命名。解構屬性值,過程中宣告一個與屬性相同名稱的 getter
const position = (x: 0, y: 2);
final (:x, :y) = position;
print('$x, $y');
更精簡的 switch 檢查,不需要 case 與 return 回傳,簡撰寫條件的同時使用 Pattern Matching,並且能使用 when
進行第二層的條件驗證,最終返回結果,可讀性也直覺。
此範例展示了我們可以直接在 UI code 根據某個狀態的變化給予相對應的內容,不需要多層的 if-else,直接使用 switch 來檢查。假設有一個教學頁面,有 5 個步驟可以切換,每個步驟顯示的文字內容都不同。
_
,通常代表為 else,並且第二個條件為不是最後一頁,這邊是指 2、3 索引_
,沒有其他條件,就是 else 本人,這邊是 4 索引也是最後一頁ElevatedButton(
onPressed: _goNext,
child: Text(
switch (_currentPage) {
0 => 'Start',
1 => 'Next',
_ when _currentPage != _LastPage => 'Next',
_ => 'Confirm',
},
),
)
範例中針對狀態都進行忽略,主要是根據第二層的 when
條件檢查,根據不同的情境顯示不一樣的內容,最後為 else 不符合的情況
switch (pageState) {
_ when pageState.isLoading => '載入中..',
_ when pageState.content.isNotEmpty => state.content,
_ => '發生未知錯誤',
}
此範例的情境是要取得 List 物件裡的倒數第2個元素,返回指定字串,如果不符合則回傳另一個結果。
_
表示,因為沒有要使用,只是為了要確保有最後一個元素List<int> numbers = [1, 2, 3];
final result = switch (numbers) {
[..., final num, _] => 'Number is $num',
[] || [_] => 'Need more numbers',
};
print(result) ;
從 Dart 3 開始,支援很多類別的修飾符,讓開發者可以精準的定義類別的擴展性,根據不同的 library file 會有不同的限制,對我們有很大的幫助。以下介紹所有修飾符:
base
class → 只允許繼承interface
class → 只允許實作final
class → 禁止繼承、實作和混合mixin
class → 混合類別。目前一般類別已經不允許當成 mxin// Failed
class NormalClass {}
class FirstClass with NormalClass {}
// Passed
mixin class MixinClass {}
class SecondClass with MixinClass {}
sealed class
→ 密封類別,針對繼承關係的操作 Compiler 會幫忙檢查,當有子類沒有處理的話就會出錯有效的 modifier 組合與使用方式可以查看官方提供的列表,告訴你每種方式是否可以建構、繼承、實作、混合,或是詳盡編譯檢查
以下表格為互斥和不適合的組合方式:
此範例使用了 sealed
class、final
class,和 switch expression 操作,讓大家更有感覺。首先情境是需要進行網路請求,並將回應分為成功與失敗兩個類別,裡面包裝對應的資料。
sealed
去定義,Success
與 Failure
繼承 Respose,泛型為我們的目標型別sealed class Response<T> {}
final class Success<T> extends Response<T> {
final T data;
Success({required this.data});
}
final class Failure<T> extends Response<T> {
final Exception exception;
Failure({required this.exception});
}
getPerson()
是我們的請求方法,回傳值為 Response<Person>
,注意中間部分,請求完之後檢查 statusCode,200的話確認成功,先將 json 解析成 Map,再透過 fromJson()
取得 Person 物件,最後回傳 Success
子類。而其他 statusCode 代表失敗,直接返回 Failure
子類Future<Response<Person>> getPerson({required int id}) async {
try {
final uri = Uri.parse('http://io.com/persons/' + id.toString());
final response = await http.get(uri);
// 1. Normal switch
switch (response.statusCode) {
case 200:
final data = json.decode(response.body);
return Success(data: Person.fromJson(data));
default:
return Failure(exception: Exception(response.reasonPhrase));
}
// 2. Switch expression
final result = switch (response.statusCode) {
200 => Success(data: Person.fromJson(json.decode(response.body))),
_ => Failure<Person>(exception: Exception(response.reasonPhrase)),
};
return result;
} on Exception catch (e) {
return Failure(exception: e);
}
}
final response = await getPerson(id: 1);
final result = switch (response) {
Success(data: final person) => person.toString(),
Failure(exception: final exception) => exception.toString(),
}
print(result);
到這裡我們簡單說明了 Dart 3 有的新東西 Record、Pattern Matching、 Class Modifier 等等用法,並且附上 10 個範例,大家應該有了解他們且迫不及待想再自己的專案上開發了。如果你覺得意猶未盡,可以閱讀我的下一篇 Dart 3 文章,會跟大家分享更多的實際案例,讓我們一起享受其中吧!
另外,如果你想看影片聽聲音學習的話,可以觀看我在 Google IO Extended 上的分享,裡面有講解到以上範例,也歡迎有時間的話將影片看完,你會更了解 Flutter 以及 Dart。以下是影片連結:
Google IO Extended 2023 - What's good in Flutter 3.10 and Dart 3?
Day 2: 使用 Dart 3 改善我們的開發習慣,更多範例與技巧分享!